/* 
 * Copyright (C) 2004-2005 Jonathan Bindel
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;

using DCSharp.Backend.Connections;
using DCSharp.Backend.Objects;
using DCSharp.Hashing;
using DCSharp.Logging;
using DCSharp.Settings;

namespace DCSharp.Backend.Managers
{
	/// <summary>
	/// The priority.
	/// </summary>
	public enum Priority
	{
		High,
		Normal,
		Low
	}

	/// <summary>
	/// Handles all the active downloaders.
	/// </summary>
	/// <remarks>
	/// <para>What happens when a <see cref="Downloader"> is added is that the
	/// connection request made by it will be handled. Once a new
	/// <see cref="UserConnection"/> is available the priority determines which
	/// downloader gets to use it first, regardless of which <see cref="Downloader">
	/// made the request.</para>
	/// </remarks>
	public class DownloadManager : IEnumerable<Downloader>, IDisposable
	{
		private static Logger log = LogManager.GetLogger("DownloadManager");

		public event EventHandler<DownloadEventArgs> DownloaderAdded;
		public event EventHandler<DownloadEventArgs> DownloaderRemoved;

		private IRuntimeSettings settings;
		private ISegmentStrategy segmentStrategy;
		private ISegmentStrategy defaultStrategy;
		private ConnectionManager connectionManager;
		private HashStore hashStore;

		private Dictionary<Downloader, Priority> downloaders;
		private Dictionary<Priority, List<Downloader>> byPriority;

		private Dictionary<User, UserConnection> connections;
		private Dictionary<User, RequestCallback> requests;
		private Dictionary<User, List<Downloader>> users;

		public DownloadManager(IRuntimeSettings settings,
			ConnectionManager connectionManager, HashStore hashStore)
		{
			if (settings == null)
			{
				throw new ArgumentNullException("settings");
			}
			if (connectionManager == null)
			{
				throw new ArgumentNullException("connectionManager");
			}
			if (hashStore == null)
			{
				throw new ArgumentNullException("hashStore");
			}
			this.settings = settings;
			this.connectionManager = connectionManager;
			this.hashStore = hashStore;

			connections = new Dictionary<User, UserConnection>();
			requests = new Dictionary<User, RequestCallback>();
			users = new Dictionary<User, List<Downloader>>();

			segmentStrategy = defaultStrategy = new Segmenter(settings);

			downloaders = new Dictionary<Downloader, Priority>();
			byPriority = new Dictionary<Priority, List<Downloader>>();
			foreach (int priority in Enum.GetValues(typeof(Priority)))
			{
				byPriority.Add((Priority)priority, new List<Downloader>());
			}

			// Events
			connectionManager.UserConnected += OnUserConnected;
			connectionManager.UserDisconnected += OnUserDisconnected;
			connectionManager.UserIdle += OnUserIdle;
		}

		#region Classes

		private class Segmenter : ISegmentStrategy
		{
			private IRuntimeSettings settings;
			private ISegmentStrategy single;
			private ISegmentStrategy multi;

			public Segmenter(IRuntimeSettings settings)
			{
				this.settings = settings;
				single = new SingleSegmentStrategy();
				multi = new MultiSegmentStrategy();
			}

			public Segment CreateSegment(SegmentedFileInfo info)
			{
				return settings.SegmentedDownloading ? multi.CreateSegment(info) :
					single.CreateSegment(info);
			}

			public int GetAvailableSegments(SegmentedFileInfo info)
			{
				return settings.SegmentedDownloading ? multi.GetAvailableSegments(info) :
					single.GetAvailableSegments(info);
			}
		}

		#endregion

		#region Properties

		/// <value>
		/// The number of handled downloaders.
		/// </value>
		public int Count
		{
			get { return downloaders.Count; }
		}

		/// <value>
		/// The default <see cref="ISegmentStrategy"/>.
		/// </value>
		public ISegmentStrategy DefaultSegmentStrategy
		{
			get { return defaultStrategy; }
		}

		/// <value>
		/// The lock to hold before enumerating over the downloaders.
		/// </value>
		public object SyncRoot
		{
			get { return downloaders; }
		}

		#endregion

		#region Methods

		#region Download

		public ListDownloader DownloadList(User user)
		{
			ListDownloader downloader = new ListDownloader(user);

			Add(downloader);
			downloader.Start();
			return downloader;
		}

		public FileDownloader Download(SearchResult result)
		{
			if (result == null)
			{
				throw new ArgumentNullException("result");
			}
			return Download(result, StringUtil.GetFileName(result.Path),
				settings.DownloadDirectory);
		}

		public FileDownloader Download(SearchResult result, string name,
			string savePath)
		{
			if (result == null)
			{
				throw new ArgumentNullException("result");
			}
			return Download(new RemoteFileInfo(name, result.Size, result.TTH),
				result.User, savePath);
		}

		public FileDownloader Download(RemoteFileInfo remoteFile, User user)
		{
			return Download(remoteFile, user, settings.DownloadDirectory);
		}

		public FileDownloader Download(RemoteFileInfo remoteFile, User user,
			string savePath)
		{
			if (remoteFile == null)
			{
				throw new ArgumentNullException("remoteFile");
			}
			if (user == null)
			{
				throw new ArgumentNullException("user");
			}
			if (savePath == null)
			{
				throw new ArgumentNullException("savePath");
			}

			FileDownloader downloader = GetDownloader(remoteFile.TTH);
			if (downloader == null)
			{
				downloader = new FileDownloader(remoteFile, savePath, hashStore,
					segmentStrategy);
			}
			downloader.AddSource(user);

			Add(downloader);
			downloader.Start();

			return downloader;
		}

		#endregion

		#region Downloaders

		/// <summary>
		/// Adds a <see cref="Downloader"/> to the list of handled downloaders.
		/// </summary>
		/// <param name="downloader">The <see cref="Downloader"/> to add.</param>
		public void Add(Downloader downloader)
		{
			if (downloader == null)
			{
				throw new ArgumentNullException("downloader");
			}
			lock (downloaders)
			{
				if (Contains(downloader))
				{
					return;
				}
				Priority priority = downloader is ListDownloader ?
					Priority.High : Priority.Normal;

				downloaders[downloader] = priority;
				byPriority[priority].Add(downloader);

				foreach (User user in downloader.InterestingUsers)
				{
					UpdateInteresting(user, downloader, true);
				}

				downloader.ConnectionRequest += OnConnectionRequest;
				downloader.StateChanged += OnStateChanged;
				downloader.UserInteresting += OnUserInteresting;
				downloader.UserUninteresting += OnUserUninteresting;
			}
			OnDownloaderAdded(downloader);
		}

		/// <summary>
		/// Removes a <see cref="Downloader"/> from the list of handled downloaders.
		/// </summary>
		/// <param name="downloader">The <see cref="Downloader"/> to remove.</param>
		public void Remove(Downloader downloader)
		{
			if (downloader == null)
			{
				throw new ArgumentNullException("downloader");
			}
			lock (downloaders)
			{
				Priority priority;
				if (!downloaders.TryGetValue(downloader, out priority))
				{
					return;
				}
				downloaders.Remove(downloader);
				byPriority[priority].Remove(downloader);

				foreach (User user in downloader.InterestingUsers)
				{
					UpdateInteresting(user, downloader, false);
				}

				downloader.ConnectionRequest -= OnConnectionRequest;
				downloader.StateChanged -= OnStateChanged;
				downloader.UserInteresting -= OnUserInteresting;
				downloader.UserUninteresting -= OnUserUninteresting;
			}
			OnDownloaderRemoved(downloader);
		}

		/// <summary>
		/// Checks if a <see cref="Downloader"/> is handled by the manager.
		/// </summary>
		/// <param name="downloader">The <see cref="Downloader"/> to check.</param>
		/// <returns>
		/// True if the <see cref="Downloader"/> is handled by the manager;
		/// otherwise, false.
		/// </returns>
		public bool Contains(Downloader downloader)
		{
			return downloaders.ContainsKey(downloader);
		}

		/// <summary>
		/// Sets the priority of a <see cref="Downloader"/>.
		/// </summary>
		/// <param name="downloader">The <see cref="Downloader"/> to set the priority for.</param>
		/// <param name="priority">The new <see cref="Priority"/>.</param>
		public void SetPriority(Downloader downloader, Priority priority)
		{
			lock (downloaders)
			{
				Priority old;
				if (downloaders.TryGetValue(downloader, out old))
				{
					downloaders[downloader] = priority;

					byPriority[old].Remove(downloader);
					byPriority[priority].Add(downloader);
				}
			}
		}

		/// <summary>
		/// Sets the <see cref="ISegmentStrategy"/> used by the downloaders. The
		/// default will create segments based on the SegmentedDownloading setting.
		/// </summary>
		/// <param name="strategy">
		/// The default <see cref="ISegmentStrategy"/> to use, or null if useDefault is enabled.
		/// </param>
		/// <param name="useDefault">Whether or not the default strategy should be used.</param>
		public void SetSegmentStrategy(ISegmentStrategy strategy, bool useDefault)
		{
			if (!useDefault && strategy == null)
			{
				throw new ArgumentNullException("strategy");
			}
			segmentStrategy = useDefault ? defaultStrategy : strategy;
			ForEach<FileDownloader>(delegate(FileDownloader downloader)
			{
				downloader.SegmentStrategy = segmentStrategy;
			});
		}

		public void ForEach<T>(Action<T> action) where T : Downloader
		{
			lock (downloaders)
			{
				foreach (Downloader downloader in downloaders.Keys)
				{
					T downloadT = downloader as T;
					if (downloadT != null)
					{
						action(downloadT);
					}
				}
			}
		}

		public T Find<T>(Predicate<T> match) where T : Downloader
		{
			lock (downloaders)
			{
				foreach (Downloader downloader in downloaders.Keys)
				{
					T downloadT = downloader as T;
					if (downloadT != null && match(downloadT))
					{
						return downloadT;
					}
				}
			}
			return null;
		}

		public IEnumerator<Downloader> GetEnumerator()
		{
			return downloaders.Keys.GetEnumerator();
		}

		/// <summary>
		/// Returns the downloaders orderd by their priority.
		/// </summary>
		/// <returns>A <see cref="IEnumerable`1"/>.</returns>
		public IEnumerable<Downloader> DownloadersByPriority()
		{
			Debug.Assert(Priority.High < Priority.Low);
			foreach (int priority in Enum.GetValues(typeof(Priority)))
			{
				foreach (Downloader downloader in byPriority[(Priority)priority])
				{
					yield return downloader;
				}
			}
		}

		IEnumerator IEnumerable.GetEnumerator()
		{
			return GetEnumerator();
		}

		public bool IsInterestedIn(User user)
		{
			return users.ContainsKey(user);
		}

		/// <summary>
		/// Returns the <see cref="FileDownloader"/> with a specific root hash.
		/// </summary>
		/// <param name="tth">The root hash.</param>
		/// <returns>The <see cref="FileDownloader"/> if found; otherwise, null.</returns>
		public FileDownloader GetDownloader(string tth)
		{
			if (tth == null)
			{
				throw new ArgumentNullException("tth");
			}
			return Find<FileDownloader>(delegate(FileDownloader downloader)
			{
				return downloader.RemoteFile.TTH == tth;
			});
		}

		public virtual void Dispose()
		{
			connectionManager.UserConnected -= OnUserConnected;
			connectionManager.UserDisconnected -= OnUserDisconnected;
			connectionManager.UserIdle -= OnUserIdle;

			lock (downloaders)
			{
				Downloader[] dispose = new Downloader[this.downloaders.Count];
				downloaders.Keys.CopyTo(dispose, 0);

				foreach (Downloader downloader in dispose)
				{
					downloader.Dispose();
				}
			}
		}

		protected virtual void OnDownloaderAdded(Downloader downloader)
		{
			if (DownloaderAdded != null)
			{
				DownloaderAdded(this, new DownloadEventArgs(downloader));
			}
		}

		protected virtual void OnDownloaderRemoved(Downloader downloader)
		{
			if (DownloaderRemoved != null)
			{
				DownloaderRemoved(this, new DownloadEventArgs(downloader));
			}
		}

		#endregion

		#region Downloading

		private void UpdateInteresting(User user, Downloader downloader,
			bool interesting)
		{
			List<Downloader> interested;
			lock (users)
			{
				if (!users.TryGetValue(user, out interested))
				{
					interested = new List<Downloader>();
					users[user] = interested;
				}
				if (interesting)
				{
					interested.Add(downloader);
					if (interested.Count == 1)
					{
						user.OnlineChanged += OnOnlineChanged;

						log.Debug("User now interesting: " + user);
						CheckIfInteresting(user);
					}
				}
				else
				{
					interested.Remove(downloader);
					if (interested.Count == 0)
					{
						user.OnlineChanged -= OnOnlineChanged;

						log.Debug("User no longer interesting: " + user);
						users.Remove(user);
					}
				}
			}
		}

		private void CheckIfInteresting(User user)
		{
			if (!CanConnectTo(user))
			{
				return;
			}

			List<Downloader> interested;
			lock (users)
			{
				if (!users.TryGetValue(user, out interested))
				{
					return;
				}
				interested = new List<Downloader>(interested);
			}

			log.Debug("Checking downloaders for interest in " + user);
			foreach (Downloader downloader in interested)
			{
				downloader.HandleIdleUser(user);
				if (!CanConnectTo(user))
				{
					return;
				}
			}
		}

		private void UseIdleConnections(Downloader downloader)
		{
			log.Debug("Using idle connections with " + downloader);
			lock (connections)
			lock (downloader.UserLock)
			{
				foreach (User user in downloader.InterestingUsers)
				{
					UserConnection connection;
					if (connections.TryGetValue(user, out connection))
					{
						if (TryUse(downloader, connection))
						{
							return;
						}
					}
				}
			}
		}

		private void UseConnection(UserConnection connection)
		{
			if (connection.Transferring)
			{
				return;
			}
			log.Debug("Using connection to " + connection.RemoteUser + " with downloaders");
			lock (downloaders)
			{
				foreach (Downloader downloader in DownloadersByPriority())
				{
					if (TryUse(downloader, connection))
					{
						return;
					}
				}
			}
			log.Debug("No downloader interested in " + connection.RemoteUser);
		}

		private bool TryUse(Downloader downloader, UserConnection connection)
		{
			lock (connection)
			{
				return !connection.Transferring && downloader.Use(connection);
			}
		}

		private bool CanConnectTo(User user)
		{
			int? slots = settings.DownloadSlots;
			bool hasSlot = slots == null ||
				(connections.Count + requests.Count) < slots;

			// A single download connection for each user is allowed.
			return hasSlot && !connections.ContainsKey(user) &&
				!requests.ContainsKey(user);
		}

		private bool RequestConnection(User user, RequestCallback callback)
		{
			lock (requests)
			{
				if (!requests.ContainsKey(user) && CanConnectTo(user) &&
					connectionManager.RequestConnection(user, OnRequestFailed))
				{
					requests.Add(user, callback);
					return true;
				}
				return false;
			}
		}

		private void InvokeRequestCallback(User user, bool success)
		{
			lock (requests)
			{
				RequestCallback callback;
				if (requests.TryGetValue(user, out callback))
				{
					log.Info("Connection request to " + user + (success ? " done" : " failed"));
					requests.Remove(user);

					callback(user, success);
				}
			}
		}

		private void OnRequestFailed(HubConnection hub, User user)
		{
			InvokeRequestCallback(user, false);
			CheckIfInteresting(user);
		}

		#region Event handlers

		private void OnUserIdle(object obj, UserEventArgs args)
		{
			CheckIfInteresting(args.User);
		}

		private void OnUserConnected(object obj, ConnectionEventArgs args)
		{
			UserConnection connection = (UserConnection)args.Connection;
			if (connection.Direction == TransferDirection.Down)
			{
				// The connection is ready for downloading.
				lock (connections)
				{
					// Only one connection from each user is used for downloading.
					if (connections.ContainsKey(connection.RemoteUser))
					{
						return;
					}
					connections.Add(connection.RemoteUser, connection);
				}

				connection.TransferCompleted += OnTransferCompleted;
				connection.TransferAborted += OnTransferAborted;

				InvokeRequestCallback(connection.RemoteUser, true);
				UseConnection(connection);
			}
			else
			{
				InvokeRequestCallback(connection.RemoteUser, false);
				CheckIfInteresting(connection.RemoteUser);
			}
		}

		private void OnUserDisconnected(object obj, ConnectionEventArgs args)
		{
			UserConnection connection = (UserConnection)args.Connection;

			connection.TransferCompleted -= OnTransferCompleted;
			connection.TransferAborted -= OnTransferAborted;

			lock (connections)
			{
				User user = connection.RemoteUser;
				if (connections.ContainsKey(user) && connections[user] == connection)
				{
					connections.Remove(connection.RemoteUser);

					InvokeRequestCallback(connection.RemoteUser, false);
				}
			}
			CheckIfInteresting(connection.RemoteUser);
		}

		private void OnTransferCompleted(object obj, EventArgs args)
		{
			UseConnection((UserConnection)obj);
		}

		private void OnTransferAborted(object obj, TransferErrorEventArgs args)
		{
			UserConnection connection = (UserConnection)obj;
			if (args.Error == TransferError.Aborted)
			{
				//OnConnectionRemoved(this, new ConnectionEventArgs((Connection)obj));

				// Remove the connection so that we don't try to use it if a
				// downloader goes idle, since the connection is disposing.
				// TODO: Add a IsDisposing property to Connection instead.
				lock (connections)
				{
					User user = connection.RemoteUser;
					if (connections.ContainsKey(user) && connections[user] == connection)
					{
						connections.Remove(connection.RemoteUser);
					}
				}
			}
		}

		private void OnConnectionRequest(object obj,
			ConnectionRequestEventArgs args)
		{
			if (!args.Handled)
			{
				args.Handled = RequestConnection(args.User, args.Callback);
			}
		}

		private void OnStateChanged(object obj, EventArgs args)
		{
			Downloader downloader = (Downloader)obj;
			if (downloader.State == DownloadState.Idle)
			{
				if (downloader.Progress < 1)
				{
					UseIdleConnections(downloader);
				}
			}
			else if (downloader.State == DownloadState.Done)
			{
				Remove(downloader);
			}
		}

		private void OnUserInteresting(object obj, UserEventArgs args)
		{
			Downloader downloader = (Downloader)obj;
			UpdateInteresting(args.User, downloader, true);

			log.Info("Using existing connection to " + args.User + " with " + downloader);
			lock (connections)
			{
				UserConnection connection;
				if (connections.TryGetValue(args.User, out connection))
				{
					TryUse(downloader, connection);
				}
			}
		}

		private void OnUserUninteresting(object obj, UserEventArgs args)
		{
			Downloader downloader = (Downloader)obj;
			UpdateInteresting(args.User, downloader, false);
		}

		private void OnOnlineChanged(object obj, EventArgs args)
		{
			User user = (User)obj;
			log.Debug("Interesting user " + user + (user.IsOnline ? " online" : " offline"));

			if (user.IsOnline)
			{
				CheckIfInteresting(user);
			}
		}

		#endregion

		#endregion

		#endregion
	}
}
